<?php
/**
 * In this File the class '\UF\TypeTool' is defined.
 *
 * @category   UniKap-Framework
 * @package    Core
 * @subpackage Helpers
 * @author     Ulf -UniKado- Kadner <ulfikado@gmail.com>
 * @since      2014-03-29 16:17
 * @version    0.1
 */

namespace UF
{

    /**
     * This class defines some static methods for helping at PHP type handling.
     *
     * @since  v0.1
     */
    class TypeTool
    {

        # <editor-fold defaultstate="collapsed" desc="- - - -   P R I V A T E   F I E L D S   - - - - - - - - - - - - - - - - - - - - -">

        /**
         * @since  v0.1
         */
        private static $rxInt32 = '~^-?(0|[1-9]\d{0,11})$~';

        /**
         * @since  v0.1
         */
        private static $rxInt64 = '~^-?(0|[1-9]\d{0,19})$~';

        /**
         * @since  v0.1
         */
        private static $rxD1 = '~^-?((0|[1-9]\d{0,20})?\.\d{0,14}|\.\d{0,14})$~';

        /**
         * @since  v0.1
         */
        private static $D='~^-?((0|[1-9]\d{0,20})?(\.|,)\d{0,14}|(\.|,)\d{1,14})$~';

        /**
         * @since  v0.1
         */
        private static $rxBoolTRUE = '~^(true|on|yes|enabled)$~i';

        /**
         * @since  v0.1
         */
        private static $rxBoolFALSE = '~^(false|off|no|enabled)$~i';

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - >   H I D D E N   C O N S T R U C T O R   - - - - - - - - - - - - - - - - -">

        /**
         * @since  v0.1
         */
        public function __construct() { }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - -">

        /**
         * Returns if the defined $value can be used as a integer value.
         *
         * @param  mixed $value The value to check
         * @return bool
         * @since  v0.1
         */
        public static function IsInteger( $value )
        {
            return (bool) \preg_match( self::$rxInt32, \strval( $value ) );
        }

        /**
         * Returns if the defined $value can be used as a decimal number value.
         *
         * @param  mixed $value            The value to check
         * @param  bool  $cultureInvariant If TRUE, the comma can be used as a decimal separator. (Defaults to FALSE)
         * @return bool
         * @since  v0.1
         */
        public static function IsDecimal( $value, $cultureInvariant = false )
        {
            if ( \preg_match( self::$rxInt64, \strval($value) ) ) return true;
            if ( !$cultureInvariant )
                return (bool) \preg_match( self::$rxD1, \strval( $value ) );
            return (bool) \preg_match( self::$D, \strval( $value ) );
        }

        /**
         * Returns if the defined $value can be used as a boolean value.
         * $resultingBoolValue returns the resulting boolean value if method returns TRUE
         *
         * @param  mixed $value              The value to check
         * @param  bool  $resultingBoolValue The resulting boolean value, if method returns TRUE.
         * @return bool
         * @since  v0.1
         */
        public static function IsBoolConvertible( $value, &$resultingBoolValue )
        {
            if ( \is_null( $value ) ) { $resultingBoolValue = false; return true; }
            if ( \is_bool( $value ) ) { $resultingBoolValue = $value; return true; }
            if ( \is_resource( $value ) ) { $resultingBoolValue = true; return true; }
            if ( \is_object( $value ) ) { $resultingBoolValue = false; return false; }
            if ( \is_array( $value ) ) {
                $resultingBoolValue = \count( $value ) > 0;
                return true; }
            if ( self::IsInteger( $value ) ) {
                $resultingBoolValue = ( \intval( $value ) > 0 );
                return true; }
            if ( self::IsDecimal( $value ) ) {
                $resultingBoolValue = ( \doubleval( $value ) > 0 );
                return true; }
            if ( \preg_match( self::$rxBoolTRUE, \strval( $value ) ) ) {
                $resultingBoolValue = true;
                return true; }
            if ( \preg_match( self::$rxBoolFALSE, \strval( $value ) ) )  {
                $resultingBoolValue = false;
                return true; }
            if ( \strlen( \strval( $value ) ) < 1 ) { $resultingBoolValue = false; return true; }
            $resultingBoolValue = false;
            return false;
        }

        /**
         * Returns if the defined $value can be used as a string value. If so, $resultingString returns the
         * value, converted to string.
         *
         * @param  mixed  $value           The value to check
         * @param  string $resultingString The resulting string, if method returns TRUE.
         * @return bool
         * @since  v0.1
         */
        public static function IsStringConvertible( $value, &$resultingString )
        {
            if ( \is_null( $value ) ) { $resultingString = ''; return true; }
            if ( \is_string( $value ) ) { $resultingString = $value; return true; }
            if ( \is_bool( $value ) ) { $resultingString = $value ? 'true' : 'false'; return true; }
            if ( \is_resource( $value ) ) { $resultingString = ''; return false; }
            if ( \is_object( $value ) )
            {
                if ( \method_exists( $value, '__toString' ) ) {
                    $resultingString = (string)$value;
                    return true; }
                if ( $value instanceof \UF\IToStringConverter ) {
                    $resultingString = $value->toString();
                    return true; }
                $resultingString = '';
                return false;
            }
            if ( \is_array( $value ) ) { $resultingString = ''; return false; }
            if ( is_int( $value ) ) { $resultingString = \strval( $value ); return true; }
            if ( self::IsDecimal( $value ) ) { $resultingString = \strval( $value ); return true; }
            $resultingString = '';
            return false;
        }

        /**
         * Converts a string to PHP type, defined in $typename.
         *
         * @param  string $string THe string to convert.
         * @param  string $typename The name of the PHP type (bool|double|...)
         * @return mixed
         * @since  v0.1
         */
        public static function StrToType( $string, $typename )
        {
            $t = new \UF\Type( $string );
            if ( !$t->hasAssociatedString() ) return null;
            $string = $t->getStringValue();
            switch ( \strtolower( $typename ) )
            {
                case 'bool': case 'boolean':
                    $res = false; self::IsBoolConvertible( $string, $res ); return $res;
                case 'float': return \floatval( \str_replace( ',', '.', $string ) );
                case 'double':
                    if ( !self::IsDecimal( $string, true ) ) return null;
                    $res = \str_replace( ',', '.', $string );
                    $tmp = \explode( '.', $res ); $ts  = \count( $tmp );
                    if ( $ts > 2 ) {
                        $dv = $tmp[$ts - 1]; unset ( $tmp[$ts - 1] );
                        $dv = \join( '', $tmp ) . '.' . $dv; return \doubleval( $dv ); }
                    return \doubleval( $res );
                case 'int': case 'integer':
                    if ( self::IsInteger( $string ) ) return \intval( $string ); return null;
                case 'string': return $string;
                case 'array':
                    if ( \strlen( $string ) < 1 ) return array();
                    if ( \strlen( $string ) > 3 ) {
                        if ( \substr( $string, 0, 2 ) == 'a:' ) {
                            try { $res = \unserialize( $string );
                                  if ( \is_array( $res ) ) return $res; }
                            catch ( \Exception $ex ) { } } }
                    return array( $string );
                default:
                    if ( \strlen( $string ) < 1 ) return null;
                    if ( \strlen( $string ) > 3 ) {
                        if ( \substr($string,0,2) == 'O:' && \preg_match('~^O:[^"]+"'.$typename.'":~',$string) )
                        {
                            try { $res = \unserialize( $string );
                                  if ( !\is_object( $res ) ) return null;
                                  if ( \get_class( $res ) == $typename ) return $res; }
                            catch ( \Exception $ex ) { } }
                    }
                    return null;
            }
        }

        /**
         * Extracts type + value data from a XML element.
         *
         * The type name can be defined by attribute 'type' or by sub element <code>&lt;Type&gt;...&lt;/Type&gt;</code>
         *
         * The value can be defined by attribute 'value' or by sub element <code>&lt;Value&gt;...&lt;/Value&gt;</code>,
         * or as a element string of $xmlelement. (<code>&lt;Element type="foo"&gt;Value here...&lt;/Value&gt;</code>
         *
         * @param  \SimpleXMLElement $xmlelement
         * @return mixed
         * @since  v0.1
         */
        public static function XmlToType( \SimpleXMLElement $xmlelement )
        {
            $type = null;
            $value = null;
            if ( isset( $xmlelement['type'] ) )     $type = (string)$xmlelement['type'];
            elseif ( isset( $xmlelement->type ) )   $type = (string)$xmlelement->type;
            elseif ( isset( $xmlelement->Type ) )   $type = (string)$xmlelement->Type;
            else                                    return null;
            if ( isset( $xmlelement['value'] ) )    $value = (string)$xmlelement['value'];
            elseif ( isset( $xmlelement->value ) )  $value = (string)$xmlelement->value;
            elseif ( isset( $xmlelement->Value ) )  $value = (string)$xmlelement->Value;
            else                                    $value = (string)$xmlelement;
            return self::StrToType( $value, $type );
        }

        /**
         * Writes all data of a value (Type + value) to the XmlWriter.
         *
         * You can do it with 3 different ways:
         *
         * - <code>&lt;$name type="..." value="..." /&gt;</code> will be created if $short is TRUE
         * - <code>&lt;$name type="..."&gt;$value&lt;/$name&gt;<code> if $short is FALSE and $separateElements is FALSE
         * - <code>&lt;$name&gt;&lt;Type&gt;...&lt;/Type&gt;&lt;Value&gt;$value&lt;/Value&gt;&lt;/$name&gt;<code>
         *   if $short is FALSE and $separateElements is TRUE
         *
         * @param \XMLWriter $w The XMLWriter
         * @param mixed      $value The value
         * @param string     $name  THe name of the XML element to create
         * @param bool       $short Use the short syntax? (Defaults to TRUE)
         * @param bool       $separateElements Write infos by separete XML subelements (Defaults to FALSE)
         * @since  v0.1
         */
        public static function WriteTypedXmlValue( \XMLWriter $w, $value, $name, $short=true, $separateElements=false )
        {
            if (!($value instanceof \UF\Type)) $value = new \UF\Type($value);
            $v = null; $t = null;
            switch ( $value->getType() )
            {
                case \UF\Type::PHP_ARRAY:
                    $v = \serialize( $value->getValue() );
                    $t = \UF\Type::PHP_ARRAY; break;
                case \UF\Type::PHP_BOOLEAN:
                    $v = $value->getValue() ? 'true' : 'false';
                    $t = \UF\Type::PHP_BOOLEAN; break;
                case \UF\Type::PHP_DOUBLE: case \UF\Type::PHP_FLOAT: case \UF\Type::PHP_INTEGER:
                    $v = $value->getValue();
                    $t = $value->getType(); break;
                case \UF\Type::PHP_NULL:
                    $v = '';
                    $t = \UF\Type::PHP_NULL; break;
                case \UF\Type::PHP_RESOURCE: # Ignore some resources
                    break;
                case \UF\Type::PHP_STRING:
                    $v = $value->getValue();
                    $t = \UF\Type::PHP_STRING; break;
                case \UF\Type::PHP_UNKNOWN:
                    $v = $value->getStringValue();
                    $t = \UF\Type::PHP_STRING; break;
                default:
                    $v = \serialize( $value->getValue() );
                    $t = $value->getType(); break;
            }
            if ( !\is_null( $t ) && !\is_null( $v ) )
            {
                $w->startElement( $name );
                if ($short) { $w->writeAttribute( 'type', $t ); $w->writeAttribute( 'value', $v ); }
                else
                {
                    if ( $separateElements ) { $w->writeElement( 'type',  $t ); $w->writeElement( 'value', $v ); }
                    else { $w->writeAttribute( 'type', $t ); $w->writeElement( 'value',  $v ); }
                }
                $w->endElement();
            }
        }

        /**
         * Returns, if a value uses a simple native PHP type. Native types are: bool, int, double, float, string
         *
         * @param  mixed $value
         * @return bool
         * @since  v0.1
         */
        public static function IsNativeType( $value )
        {
            return (
                \is_bool(   $value ) ||
                \is_int(    $value ) ||
                \is_string( $value ) ||
                \is_double( $value ) ||
                \is_float(  $value ) );
        }

        /**
         * Converts a value of type <b>null</b>, <b>int</b>, <b>float</b>, <b>double</b>, <b>bool</b>,
         * <b>\UF\DateTime</b>, <b>array</b>, <b>object</b> in a XML element.
         *
         * The resulting XML element for $value=30 and $valueName='age' looks like this:
         *
         * <code>&lt;element name="age" type="int"&gt;30&lt;/element&gt;</code>
         *
         * If the third parameter $forceShortXml is TRUE, for types <b>null</b>, <b>int</b>, <b>float</b>,
         * <b>double</b>, <b>bool</b> and <b>\UF\DateTime</b> a XML with the following format is created:
         *
         * <code>&lt;element name="age" type="int" value="30"/&gt;</code>
         *
         * @param  mixed $value        The value to create a XML element string for
         * @param  string $valueName   The name of the value.
         * @param  bool $forceShortXml For types NULL, int, double, float, bool and \UF\DateTime use the short, self
         *                             closing XML element syntax?
         * @return string XML or bool FALSE if $value is of type 'resource'.
         * @since  v0.1
         */
        public static function TypeToXml2( $value, $valueName, $forceShortXml=false )
        {
            if ( \is_resource( $value ) ) return false;
            if ( \is_null( $value ) ) return \sprintf('<element name="%s" type="NULL" />', $valueName);
            if ( \is_int( $value ) || \is_double( $value ) || \is_float( $value ) )
                return $forceShortXml
                    ? \sprintf(
                        '<element name="%s" type="%s" value="%s"/>', $valueName, \gettype($value), \strval($value) )
                    : \sprintf(
                        '<element name="%s" type="%s">%s</element>', $valueName, \gettype($value), \strval($value) );
            if ( \is_bool( $value ) )
                return $forceShortXml
                    ? \sprintf( '<element name="%s" type="bool" value="%s"/>', $valueName, ($value ? '1' : '0') )
                    : \sprintf( '<element name="%s" type="bool">%s</element>', $valueName, ($value ? '1' : '0') );
            if ( $value instanceof \UF\DateTime )
                return $forceShortXml
                    ? \sprintf( '<element name="%s" type="\\UF\\DateTime" value="%s"/>', $valueName, $value->stamp )
                    : \sprintf( '<element name="%s" type="\\UF\\DateTime">%s</element>', $valueName, $value->stamp );
            if ( \is_array( $value ) || \is_object( $value ) )
            {
                $t = \is_array( $value ) ? 'array' : 'object';
                return \sprintf(
                    '<element name="%s" type="%s"><![CDATA[%s]]></element>', $valueName, $t, \serialize( $value ) );
            }
            try
            {
                $strval = \strval( $value );
                return \sprintf( '<element name="%s" type="string"><![CDATA[%s]]></element>', $valueName, $strval );
            }
            catch ( \Exception $ex ) { $ex = null; return ''; }
        }

        /**
         * Converts a XML-Element, created with {@see \UF\TypeTool::TypeToXml2()} back to a PHP value.
         *
         * Requires a XML format like:
         *
         * <code>&lt;element name="age" type="int" value="30"/&gt;</code>
         *
         * or:
         *
         * <code>&lt;element name="age" type="int"&gt;30&lt;/element&gt;</code>
         *
         * It returns the data in $resultSet. After you call this method $resultSet have a new item. The key of the new
         * item ist the Name of the value. The associated casted value is also the array value.
         *
         * @param  \SimpleXMLElement $xmlelement SimpleXMLElement
         * @param  array $resultSet  Here the resulting data will be stored
         * @throws \UF\ArgumentFormatException If XML element attribute 'name' and/or 'type' is missed.
         * @since  v0.1
         */
        public static function XmlToType2( \SimpleXMLElement $xmlelement, array &$resultSet )
        {
            if ( !isset( $xmlelement['name'] ) || !isset( $xmlelement['type'] ) )
                throw new \UF\ArgumentFormatException(
                    'xmlelement', $xmlelement, \UF\_( '_core',
                    'Not enougth xml element attributes! Converting a SimpleXMLElement into a typed value fails.' ) );
            if ( isset($xmlelement['value'] ) )
            {
                $resultSet[(string)$xmlelement['name']] = self::StrToType(
                    (string)$xmlelement['value'],
                    (string)$xmlelement['type']
                );
                return;
            }
            $resultSet[(string)$xmlelement['name']] = self::StrToType(
                (string)$xmlelement,
                (string)$xmlelement['type']
            );
        }

        /**
         * Returns the native PHP type of the defined value, or FALSE if $value does not have a native PHP type.
         * Native types are: <b>bool</b>, <b>int</b>, <b>double</b>, <b>float</b> and <b>string</b>
         *
         * @param type $value
         * @return string Native type name, or (boolean)FALSE (see {@see \UF\Type}::PHP_* constants)
         * @since  v0.1
         */
        public static function GetNativeType( $value )
        {
            if ( \is_string($value) ) return \UF\Type::PHP_STRING;
            if ( \is_int($value) ) return \UF\Type::PHP_INTEGER;
            if ( \is_bool($value) ) return \UF\Type::PHP_BOOLEAN;
            if ( \is_double($value) ) return \UF\Type::PHP_DOUBLE;
            if ( \is_float($value) ) return \UF\Type::PHP_FLOAT;
            return false;
        }

        /**
         * Return the name of the PHP type for $value, defined in the {@see \UF\Type}::PHP_* constants
         *
         * @param type $value The value
         * @return string Type name (see {@see \UF\Type}::PHP_* constants)
         * @since  v0.1
         */
        public static function GetTypeName( $value )
        {
            if ( \is_null($value) ) return \UF\Type::PHP_NULL;
            if ( \is_object($value) ) return \get_class($value);
            if ( \is_string($value) ) return \UF\Type::PHP_STRING;
            if ( \is_int($value) ) return \UF\Type::PHP_INTEGER;
            if ( \is_bool($value) ) return \UF\Type::PHP_BOOLEAN;
            if ( \is_double($value) ) return \UF\Type::PHP_DOUBLE;
            if ( \is_float($value) ) return \UF\Type::PHP_FLOAT;
            if ( \is_resource($value) ) return \UF\Type::PHP_RESOURCE;
            if ( \is_array($value) ) return \UF\Type::PHP_ARRAY;
            return \UF\Type::PHP_UNKNOWN;
        }

        /**
         * Converts a value of a native PHP type <b>bool</b>, <b>int</b>, <b>double</b>, <b>float</b> and <b>string</b>
         * to a other native PHP type.
         *
         * @param  mixed  $sourceValue The value to convert
         * @param  string $newType Native PHP typ To convert. (see {@see \UF\Type}::PHP_* constants)
         * @return mixed
         * @throws \UF\ArgumentOutOfRangeException If $value not uses a valid native PHP type.
         * @since  v0.1
         */
        public static function ConvertNative( $sourceValue, $newType )
        {
            if ( \is_null($sourceValue) ) return $sourceValue;
            if (false === ($sourceType = self::GetNativeType($sourceValue)))
                throw new \UF\ArgumentOutOfRangeException( 'sourceValue', $sourceValue, \UF\_( '_core',
                    'Can not convert a value of a type that isnt a native PHP type! (bool, int, double, float, string)'
                ) );
            if ( $sourceType == $newType ) return $sourceValue;
            switch ( $sourceType ) {
                case \UF\Type::PHP_BOOLEAN:
                    switch ( $newType ) {
                        case \UF\Type::PHP_DOUBLE:  return (double)($sourceValue ? 1 : 0);
                        case \UF\Type::PHP_FLOAT:   return (float)($sourceValue ? 1 : 0);
                        case \UF\Type::PHP_INTEGER: return ($sourceValue ? 1 : 0);
                        case \UF\Type::PHP_STRING:  return ($sourceValue ? '1' : '0'); }
                    break;
                case \UF\Type::PHP_DOUBLE:
                    switch ( $newType ) {
                        case \UF\Type::PHP_FLOAT:   return \doubleval( $sourceValue );
                        case \UF\Type::PHP_INTEGER: return \intval( $sourceValue );
                        case \UF\Type::PHP_STRING:  return '' . $sourceValue;
                        case \UF\Type::PHP_BOOLEAN: return $sourceValue > 0; }
                    break;
                case \UF\Type::PHP_FLOAT:
                    switch ( $newType ) {
                        case \UF\Type::PHP_DOUBLE:  return \floatval( $sourceValue );
                        case \UF\Type::PHP_INTEGER: return \intval( $sourceValue );
                        case \UF\Type::PHP_STRING:  return '' . $sourceValue;
                        case \UF\Type::PHP_BOOLEAN: return $sourceValue > 0; }
                    break;
                case \UF\Type::PHP_INTEGER:
                    switch ( $newType ) {
                        case \UF\Type::PHP_DOUBLE:  return \intval( $sourceValue );
                        case \UF\Type::PHP_FLOAT:   return \intval( $sourceValue );
                        case \UF\Type::PHP_STRING:  return '' . $sourceValue;
                        case \UF\Type::PHP_BOOLEAN: return $sourceValue > 0; }
                    break;
                case \UF\Type::PHP_STRING:
                    switch ( $newType ) {
                        case \UF\Type::PHP_INTEGER: case \UF\Type::PHP_BOOLEAN: case \UF\Type::PHP_FLOAT:
                        case \UF\Type::PHP_DOUBLE: return \UF\TypeTool::StrToType($sourceValue, $newType); }
                    break;
            }
            throw new \UF\ArgumentOutOfRangeException( 'newType', $newType, \UF\_( '_core',
                'Can not convert a value of a type that isnt a native PHP type! (bool, int, double, float, string)' ) );
        }

        # </editor-fold>

    }

}

